Asynchronous programming is a first-class citizen in Python now. If you’re a web developer, there are amazing frameworks you can choose from!
As of writing, asynchronous is no more just a buzzword in the Python community. With the release of its asyncio library in 3.5 version, Python acknowledged the impact of Node.js on web development and introduce two new keywords into the language — async
and await
.
This was a very big deal because the Python language is extremely wary of expanding the core syntax unless there’s a pressing need, which only indicates how fundamentally important the Python developers considered the asynchronous capabilities.
As a result, floodgates of asynchronous programming were opened: libraries new and old started making use of the coroutines feature, asynchronous frameworks exploded in popularity, and new ones are still being written today.
Performance at par with or better than Node.js’s isn’t unheard of, and unless your loading patterns involve plenty of CPU-heavy tasks, there’s no reason why you can’t make a few thousand requests per second.
But enough motivation!
Let’s survey the current Python landscape and check out some of the top asynchronous frameworks.
Tornado
Surprisingly, Tornado isn’t a new framework at all. Its initial release was in 2009 and since then, its focus has been on providing rock-solid asynchronous programming with high concurrency.
Tornado isn’t a web framework fundamentally. It’s a collection of asynchronous modules, which are also used to build the web framework module. More specifically, these modules are:
- Coroutines and other primitives (
tornado.gen
,tornado.locks
,tornado.queues
, etc.) - Networking modules (
tornado.ioloop
,tornado.iostream
, etc.) - Asynchronous servers and clients (
tornado.httpserver
,tornado.httpclient
, etc.)
These have been combined to produce the final framework modules: tornado.web
, tornado.routing
, tornado.template
, etc.
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
Tornado has a strong and committed following in the Python community and is used by experienced architects to build highly capable systems. It’s a framework that has long had the answer to the problems of concurrency but perhaps didn’t become mainstream as it doesn’t support the WSGI standard and was too much of a buy-in (remember that the bulk of Python libraries are still synchronous).
Sanic
Sanic is a “modern” framework in the true sense of the word: it doesn’t support Python version below 3.6, supports the simple and universal async/await syntax out of the box, and as a result, doesn’t make you read loads of documentation and keep edge cases in your mind before you can write your first HTTP handler.
As a result, the resulting syntax is quite pleasant (in my opinion, at least); it resembles code you’d write with any other microframework (Flask, CherryPy, for example) with just a few async
sprinkled in:
from sanic import Sanic
from sanic.response import json
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
Sanic is arguably the most popular and most loved async framework in the Python world. It has almost all features that you’d want for your projects — routing, middleware, cookies, versioning, blueprints, class-based views, static files, streaming, sockets, etc. — and what it doesn’t offer out of the box — templating, database support, file I/O, queues — can be added as there are just enough async libraries for these as of today.
Vibora
Vibora is a close cousin of Sanic, except that it’s fixated on becoming the fastest Python web server out there. In fact, the very first visit of its website greets you with a framework comparison:
As you can see, Vibora claims to be several times faster than the classic frameworks and being more than twice as fast as Sanic, its nearest competitor. Of course, benchmarks are to be taken with a grain of salt. ๐
Although in syntax and features, Vibora is comparable to Sanic (or maybe even slightly better as it bundles popular libraries and things like templating are available out of the box), I’d consider Sanic to be more mature as it’s been around longer and has a bigger community.
from vibora import Vibora, JsonResponse
app = Vibora()
@app.route('/')
async def home():
return JsonResponse({'hello': 'world'})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
If you’re a performance junkie, though, Vibora might float your boat. That said, as of writing Vibora is underdoing a complete rewrite to become even faster, and the link to its performance version says it’s under “heavy development.” It’s going to be a let down for those who picked up Vibora earlier and soon must face breaking changes, but hey, it’s early days in Python async world, and nobody expects things to be stable.
Quart
If you enjoy developing in Flask but rue the lack of async support, you’ll enjoy Quart a lot.
Quart is compliant with the ASGI standard, which is a successor to the famous WSGI standard and offers async support. The interesting thing about Quart is that it’s not only similar to Flask but is actually compliant with the Flask API! The author of this framework wanted to preserve the Flask feel and just add async, WebSockets, and HTTP 2 support to it. As a result, you can learn Quart right from the Flask documentation, just keeping in mind that functions in Quart are asynchronous.
from quart import Quart
app = Quart(__name__)
@app.route('/')
async def hello():
return 'hello'
app.run()
Feels (almost) exactly like Flask, doesn’t it?!
Since Quart is an evolution of Flask, all the features inside Flask are available: routing, middleware, sessions, templating, blueprints, and so on. In fact, you can even use Flask extensions directly inside Quart. One catch is that Python 3.7+ is only supported, but, then, if you’re not running the latest version of Python, maybe async isn’t the right path. ๐
The documentation is really wanting if you don’t have earlier experience with Flask, but I can recommend Quart as it’s probably the only async framework nearing its 1.0 release soon.
FastAPI
The last (but most impressive) framework on this list is FastAPI. No, it’s not an API-only framework; in fact, FastAPI seems to be the most feature-rich and documentation-rich framework that I came across when researching async Python frameworks.
It’s interesting to note that the framework author studied several other frameworks in-depth, from the contemporary ones like Django to modern ones like Sanic, as well as looking across technologies into NestJS (a Node.js, Typescript web framework). Their development philosophy and extensive comparisons can be read here.
The syntax is quite pleasant; one can even argue it’s much more enjoyable than the other frameworks we’ve come across:
rom fastapi import FastAPI
app = FastAPI()
@app.get("/users/me")
async def read_user_me():
return {"user_id": "the current user"}
@app.get("/users/{user_id}")
async def read_user(user_id: str):
return {"user_id": user_id}
And now, the list of killer features that make FastAPI outshine other frameworks:
Automatic API doc generation: As soon as your endpoints have been written, you can play with the API using a standards-compliant UI. SwaggerUI, ReDoc, and others are supported.
The framework also does automatic data model documentation with JSON Schema.
Modern development: Yes, the word “modern” gets thrown around a lot, but I found FastAPI actually to walk its talk. Dependency Injection and type hinting are first-class citizens, enforcing not just good coding principles but preventing bugs and confusion in the long run.
Extensive documentation: I don’t know about you, but I’m a total sucker for good documentation. And in this area, FastAPI wins hands-down. It has pages upon pages of docs explaining almost every little subtlety and “watch out!” moments for developers of all levels. I sense a clear “heart and soul” in the docs here, and the only comparison I can find is the Django docs (yes, FastAPI docs are that good!).
Beyond the basics: FastAPI has support for WebSockets, Streaming, as well as GraphQL, besides having all the traditional helpers like CORS, sessions, cookies, and so forth.
And what about the performance? Well, FastAPI is built on the amazing Starlette library, resulting in performance that matches Node, and in some cases, even Go! All in all, I really have the feeling that FastAPI is going to race ahead as the top async framework for Python.
BlackSheep
BlackSheep can be used to create server-side or full-stack applications with an MVC pattern.
Some of the features that BlackSheep offers are
- A rich-code API.
- Built-in dependency injection.
- Built-in generation of OpenAPI documentation.
- Automatic binding of request handlers.
Project Setup
Let’s create a basic server-side application with BlackSheep. Quickly run the below commands one by one to set up the project.
python -m venv basic-app
cd basic-app
source bin/activate
pip install blacksheep uvicorn
Learn how to install PIP package installer on CentOS, Ubuntu and Windows.
We are done with setting up the project. Let’s create a file called server.py
and place the following code.
from blacksheep import Application
app = Application()
@app.router.get("/")
def home():
return "Hello, World!"
We have created our most famous hello world application. There is only one route with HTTP GET
method for now, and it’s /. The function home
is called request handler in BlackSheep.
We have used a router
decorator from the app. There is another way to create routes i.e.., route. We will be using the router
in this tutorial. You can find more about route
in the docs.
Let’s run the application with the following command.
uvicorn server:app --port 8000 --reload
Go to the http://localhost:8000/
in the browser. You will see hello world in the browser. Let’s talk a bit about the command that we used to run the application.
- We have used
uvicorn
package to run our application. - The
server
is the file name that we have given. If you use a different file name, change it in the start command as well. - The option
--port
is give the port on which our app should run. - Finally, the
--reload
option is to reload the application in the browser whenever we make changes to theserver
file.
JSON Response
In the real world, we need the API responses in JSON in most cases. We can return a JSON response from the method by wrapping the JSON object with json
from the blacksheep
package. Let’s see how we can do it.
from blacksheep import Application, json
app = Application()
@app.router.get("/")
def home():
return json({"message": "Hello, World!"})
We have imported json
from the blacksheep
and wrapped the JSON object with it. Check it in the browser for JSON response.
Route Parameters
We need to accept the route params sometimes for the requests. We can do it in BlackSheep by defining them inside the HTTP method. Let’s see it.
@app.router.get("/{name}")
def home(name):
return json({"greetings": f"Hello, {name}!"})
We are accepting one route parameter called name
. Go to the http://localhost:8000/Geekflare
. The method will return a greeting with the name given in the route.
The router decorator will pass the parameter with the same name to the home
function as it’s given to the decorator. Here, it will be name
. If you change it in the decorator, change it in the home
function as well.
We can accept as many route parameters as possible in a similar way. Let’s see a quick example.
@app.router.get("/{name}/{info}")
def home(name, info):
return json({"greetings": f"Hello, {name}! Info {info}"})
We have accepted one more route parameter called info
. Go to http://localhost:8000/Geekflare/Chandan to check it.
Query Parameters
We don’t need to do anything to accept the query parameters. BlackSheep will automatically send the query parameters to the function in the form of a list. Let’s see the example.
@app.router.get("/")
def home(name):
print(name)
return json({"greetings": f"Hello, {name[0]}!"})
Go to http://localhost:8000/?name=Geekflare to check the response. If you have multiple query parameters with the same name, BlackSheep will add all of them to the list.
Go to http://localhost:8000/?name=Geekflare&name=Chandan and check the output in the terminal. You will see a list with Geekflare
and Chadan
as we have passed two query parameters with the same name.
If you want multiple query parameters with different, you can do it too. Just add another argument to the function with the query parameter name and do what you want with it.
Request Object
The only thing left in our basics is checking other HTTP methods. Before going into it, let’s check the request
object for the API.
All the request handlers in the BalckSheep will have a request argument which contains all the information of the coming request. It includes request headers, path parameters, query parameters, data, etc..,
Let’s see an example to see the request object.
@app.router.post("/")
def home(request):
print(request)
return "Hello, World!"
You can see the following output in the terminal.
<Request POST />
We can access different things from the request. You can check the docs for it. Here, our focus is on the request body. Let’s see how to access the request body from the request object.
@app.router.post("/")
async def home(request):
data = await request.json()
print(data)
return "Hello, World!"
There is a method called json
in the request, which will return the data that’s coming from the request. Pass some data in the API request and call it. You will see the data printing in the terminal that you have passed to the API.
HTTP Methods
We have seen the GET and POST methods in the above examples. Similarly, you can use the PUT, DELETE, etc.., methods as well. Trying yourself won’t be a problem as they are straightforward.
AIOHTTP
aiohttp is another framework that comes with the following key features.
- It supports both server-side and client-side WebSockets.
- It supports both server and client application development.
- Its web server has middleware, signals, and pluggable routing.
Project Setup
Let’s create a basic server-side application with aiohttp. Quickly run the following commands to set up the project.
python -m venv basic-app
cd basic-app
source bin/activate
pip install aiohttp aiodns
Create a file called server.py
and place the following code in it.
from aiohttp import web
async def home(request):
return web.Response(text="Hello, World!")
app = web.Application()
app.add_routes([web.get('/', home)])
web.run_app(app)
The web.Application
instance is our main application. We have added the HTTP GET method with the route /
which returns our favorite hello world. The web.run_app function is used to run the application, which takes the web.Application
instance as an argument.
The function home
is called a request handler in aiohttp. And it has one, and only argument called request, which contains all the information of the incoming request.
Run the application with the following command. It’s the same as running normal python programs.
python3 server.py
Go to http://localhost:8080/
in the browser. You will see hello world in the browser.
JSON Response
We can return the response in the JSON format using web.json_response
function. Pass the JSON data to that function while returning the response. Let’s see the example.
async def home(request):
return web.json_response({"message": "Hello, World!"})
If you go to http://localhost:8080/
you will see the JSON object in the browser.
Route Parameters
We can define the route parameters while adding the routes. And they can be accessed from the request
argument of the request handler. Let’s see an example.
from aiohttp import web
async def home(request):
return web.json_response({"message": f"Hello, {request.match_info['name']}!"})
app = web.Application()
app.add_routes([web.get('/{name}', home)])
web.run_app(app)
All route parameters can be accessed from the request.match_info
as shown in the above example. Go to http://localhost:8080/Geekflare
to check it.
We can also have regex to match the routes. Let’s say we have to accept only /{any_number}
. We can do it by replacing the '/{name}'
with r'/{number:\d+}'
. We have added regex to the path parameter, which will accept only if the regex is passed.
Let’s see an example
from aiohttp import web
async def home(request):
return web.json_response({"message": f"Hello, {request.match_info['number']}!"})
app = web.Application()
app.add_routes([web.get(r'/{number:\d+}', home)])
web.run_app(app)
Go to http://localhost:8080/Geekflare, you will 404 error as Geekflare
doesn’t match the regex pattern that we have given. Now, go to http://localhost:8080/1234567890
you will see the response in the browser.
Query Parameters
There is no need to add anything to accept query parameters. We can accept the query parameters from the request.query
object. Let’s see an example.
from aiohttp import web
async def home(request):
return web.json_response({"message": f"Hello, {request.query.get('name')}"})
app = web.Application()
app.add_routes([web.get('/', home)])
web.run_app(app)
Go to http://localhost:8080/?name=Geekflare
and check the result. You will see Geekflare
in the response that we are accessing from the request.query
.
We can pass multiple query parameters as well that can be accessed with their key names.
HTTP Methods
We have seen how to create a HTTP GET method in the above examples. We need to know how to access the request data before moving ahead. Let’s see an example of it.
from aiohttp import web
async def home(request):
data = await request.json()
print(data)
return web.json_response({"message": f"Hello, World!"})
app = web.Application()
app.add_routes([web.post('/', home)])
web.run_app(app)
In the above example, we have changed the API method from GET to POST. We have accessed the request data using request.json
method.
Make a post request to http://localhost:8080/
, you will see the request data printing in the terminal.
Similarly, you can use the PUT, DELETE, etc.., methods as well. Try them yourself and have fun.
You can continue exploring more about the framework in their docs.
Falcon
Falcon is an ASGI framework to build REST APIs and microservices. It has the following key features.
- It supports WebSockets.
- Supports middleware and hooks for request processing.
- Simple and straightforward exception handling.
Project Setup
Let’s set up a project to learn the basics of the falcon framework. Set up the project with the following commands.
python -m venv basic-app
cd basic-app
source bin/activate
pip install falcon uvicorn
Create a file called server.py
and place the following code.
from falcon import asgi
import falcon
class Home:
async def on_get(self, request, response):
response.status = falcon.HTTP_200 # This is the default status
response.content_type = falcon.MEDIA_TEXT # Default is JSON, so override
response.text = 'Hello, World!'
app = asgi.App()
app.add_route('/', Home())
We have created a class with on_get
method which is HTTP GET method. The method has two arguments. One is request
and another one if response
. You should have guessed what they are by their names themselves.
The request
argument contains all the information of the incoming request which can be accessed to process the request. And the response
argument is used to send the response by setting different things.
Unlike BlackSheep and AIOHTTP we don’t have to return a response. We can use the response and set whatever details we need to send as a response. In the above example, we have set the status to 200, the content type to text, and the text to hello world.
Run the application with the following command
uvicorn server:app --reload
Go to http://localhost:8000/
, you will see the hello world as a response.
JSON Response
We can return the response as JSON by converting the data into JSON using json.dumps
method. Let’s see an example
from falcon import asgi
import falcon
import json
class Home:
async def on_get(self, request, response):
response.text = json.dumps({"greetings": "Hello, World!"})
app = asgi.App()
app.add_route('/', Home())
Go to http://localhost:8000/
, you will see the response in JSON.
Route Parameters
The request parameters are passed to the HTTP methods as arguments. You can see the example below for a better understanding.
from falcon import asgi
import falcon
import json
class Home:
async def on_get(self, request, response, name):
response.text = json.dumps({"greetings": f"Hello, {name}!"})
app = asgi.App()
app.add_route('/{name}/', Home())
The name
path parameter will be passed to on_get
method as an argument. Go to http://localhost:8000/Geekflare/
and check the response. We can have as many route parameters as we want.
Query Parameters
We can access the query parameters using request.get_param(param_name)
method. Check out the example below.
from falcon import asgi
import falcon
import json
class Home:
async def on_get(self, request, response):
response.text = json.dumps({"greetings": f"Hello, {request.get_param('name')}!"})
app = asgi.App()
app.add_route('/', Home())
Go to http://localhost:8000/?name=Geekflare
to check the response. We can have as many query parameters as we want.
HTTP Methods
We have seen the GET method in the above example. The one thing we need to know for other methods is how to access request data. We can access the request data using request.stream.read
method. Let’s see an example.
from falcon import asgi
import falcon
import json
class Home:
async def on_get(self, request, response):
response.text = json.dumps({"greetings": "Hello, World!"})
async def on_post(self, request, response):
data = await request.stream.read()
print(json.loads(data))
response.text = "Hello, World!"
app = asgi.App()
app.add_route('/', Home())
We have added one POST method in which we accessed the request data and printed it to the terminal after converting it to JSON. Make a POST request with some data to check the output.
Try adding other HTTP methods like DELETE, PUT, etc.., by yourself. We have converted only the basics of the falcon framework. But, there is a lot in it. Go and read the docs to find out more deeply about it.
Starlette
Starlette is a lightweight ASGI framework in Python. It has similar almost all the basic features to build server-side applications.
Set up the project with the following commands.
python -m venv basic-app
cd basic-app
source bin/activate
pip install starlette uvicorn
Creating APIs with Starlette is similar to what we have seen in the last frameworks. The syntax and way of creating APIs are different. All the concepts remain the same. So, we are going to include all the things in a single program.
Create a file called server.py
and place the following code.
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse, JSONResponse
from starlette.routing import Route
def homepage(request):
return PlainTextResponse('Hello, World!')
def json_response(request):
return JSONResponse({'message': 'Hello, World!'})
def path_params(request):
name = request.path_params['name']
return JSONResponse({'greetings': f'Hello, {name}!'})
def query_params(request):
name = request.query_params['name']
return JSONResponse({'greetings': f'Hello, {name}!'})
async def post_method(request):
data = await request.json()
print(data)
return JSONResponse({'message': f'Hello, World!'})
def startup():
print('Starlette started')
routes = [
Route('/', homepage),
Route('/json', json_response),
Route('/path-params/{name}', path_params),
Route('/query-params', query_params),
Route('/post', post_method, methods=['POST']),
]
app = Starlette(debug=True, routes=routes, on_startup=[startup])
Run the application with the following command.
uvicorn server:app --reload
Test all things that we have seen in the previous frameworks. You can learn more about the Starlette framework in its docs.
Conclusion
A lot is going on in the Python async landscape these days. New frameworks are popping up, old ones are being rewritten, and libraries are being evolved to match async behavior. While Python has built-in support for an event loop, and it’s possible to make parts of your application async, you can choose to go all-in and build on one of the frameworks here.
Just be sure to keep the long-term in mind: several of the Python async frameworks out there are in the early stages and are being rapidly evolved, which is going to hurt your development process and raise business costs.
Caution is key!
But all said and done; Python is production-ready to deliver light-out performance when it comes to web frameworks. If for so long you’ve been thinking of migrating to Node, now you don’t need to! ๐
Sounds cool? Master Python today!